
#include "sbAlbumViewScan.h"

#include <sbILibraryManager.h>
#include <sbILibrary.h>
#include <sbIFilterableMediaListView.h>
#include <sbILibraryConstraints.h>
#include <sbISortableMediaListView.h>
#include <sbPropertiesCID.h>
#include <sbStandardProperties.h>
#include <sbIPropertyArray.h>
#include <sbStringUtils.h>

#include <nsArrayUtils.h>
#include <nsComponentManagerUtils.h>
#include <nsServiceManagerUtils.h>
#include <nsStringAPI.h>
#include <nsIUUIDGenerator.h>

/**
 * To log this module, set the following environment variable:
 *   NSPR_LOG_MODULES=sbAlbumViews:5
 * Use the following to output to a file:
 *   NSPR_LOG_FILE=path/to/file.log
 */
#include "prlog.h"
#ifdef PR_LOGGING
static PRLogModuleInfo* gAlbumViewsLog = nsnull;
#define TRACE(args) PR_LOG(gAlbumViewsLog, PR_LOG_DEBUG, args)
#define LOG(args)   PR_LOG(gAlbumViewsLog, PR_LOG_WARN, args)
#else
#define TRACE(args) /* nothing */
#define LOG(args)   /* nothing */
#endif /* PR_LOGGING */

#ifndef kNotFound
#define kNotFound -1
#endif


// How many tracks to process before relinquishing control to the UI
#define SCAN_BATCH_SIZE 40

//------------------------------------------------------------------------------
//
// nsISupports implementation.
//
//------------------------------------------------------------------------------
NS_IMPL_THREADSAFE_ISUPPORTS4(sbAlbumViewScan,
                              sbIAlbumViewScan,
                              sbIJobProgress,
                              sbIJobProgressUI,
                              sbIJobCancelable)

//------------------------------------------------------------------------------
//
// sbIJobProgress implementation.
//
//------------------------------------------------------------------------------

/* readonly attribute unsigned short status; */
NS_IMETHODIMP
sbAlbumViewScan::GetStatus(PRUint16* aStatus)
{
  TRACE(("sbAlbumViewScan[0x%8.x] - GetStatus", this));
  NS_ENSURE_ARG_POINTER( aStatus );
  *aStatus = mStatus;
  return NS_OK;
}

/* readonly attribute unsigned AString statusText; */
NS_IMETHODIMP
sbAlbumViewScan::GetStatusText(nsAString& aText)
{
  TRACE(("sbAlbumViewScan[0x%8.x] - GetStatusText", this));

  if (mStatus == sbIJobProgress::STATUS_RUNNING) {
    aText.AssignLiteral("Scanning for albums.");
  } else {
    aText.AssignLiteral("Scanning completed.");
  }
  return NS_OK;
}

/* readonly attribute AString titleText; */
NS_IMETHODIMP
sbAlbumViewScan::GetTitleText(nsAString& aText)
{
  TRACE(("sbAlbumViewScan[0x%8.x] - GetTitleText", this));

  aText.AssignLiteral("Album Scanner.");
  return NS_OK;
}


/* readonly attribute unsigned long progress; */
NS_IMETHODIMP
sbAlbumViewScan::GetProgress(PRUint32* aProgress)
{
  TRACE(("sbAlbumViewScan[0x%8.x] - GetProgress", this));
  NS_ENSURE_ARG_POINTER( aProgress );

  *aProgress = mCurrentItemCount;
  return NS_OK;
}

/* readonly attribute unsigned long total; */
NS_IMETHODIMP
sbAlbumViewScan::GetTotal(PRUint32* aTotal)
{
  TRACE(("sbAlbumViewScan[0x%8.x] - GetTotal", this));
  NS_ENSURE_ARG_POINTER( aTotal );

  *aTotal = mTotalItemCount;
  return NS_OK;
}

/* readonly attribute unsigned long errorCount; */
NS_IMETHODIMP
sbAlbumViewScan::GetErrorCount(PRUint32* aCount)
{
  TRACE(("sbAlbumViewScan[0x%8.x] - GetErrorCount", this));
  NS_ENSURE_ARG_POINTER( aCount );

  *aCount = 0;
  return NS_OK;
}

/* nsIStringEnumerator getErrorMessages(); */
NS_IMETHODIMP
sbAlbumViewScan::GetErrorMessages(nsIStringEnumerator** aMessages)
{
  TRACE(("sbAlbumViewScan[0x%8.x] - GetErrorMessages", this));
  NS_ENSURE_ARG_POINTER(aMessages);

  *aMessages = nsnull;
  return NS_OK;
}

/* void addJobProgressListener( in sbIJobProgressListener aListener ); */
NS_IMETHODIMP
sbAlbumViewScan::AddJobProgressListener(sbIJobProgressListener *aListener)
{
  TRACE(("sbAlbumViewScan[0x%8.x] - AddJobProgressListener", this));
  NS_ENSURE_ARG_POINTER(aListener);

  PRInt32 index = mListeners.IndexOf(aListener);
  if (index >= 0) {
    // the listener already exists, do not re-add
    return NS_SUCCESS_LOSS_OF_INSIGNIFICANT_DATA;
  }
  PRBool succeeded = mListeners.AppendObject(aListener);
  return succeeded ? NS_OK : NS_ERROR_FAILURE;
}

/* void removeJobProgressListener( in sbIJobProgressListener aListener ); */
NS_IMETHODIMP
sbAlbumViewScan::RemoveJobProgressListener(sbIJobProgressListener* aListener)
{
  TRACE(("sbAlbumViewScan[0x%8.x] - RemoveJobProgressListener", this));
  NS_ENSURE_ARG_POINTER(aListener);

  PRInt32 indexToRemove = mListeners.IndexOf(aListener);
  if (indexToRemove < 0) {
    // Err, no such listener
    return NS_ERROR_UNEXPECTED;
  }

  // remove the listener
  PRBool succeeded = mListeners.RemoveObjectAt(indexToRemove);
  NS_ENSURE_TRUE(succeeded, NS_ERROR_FAILURE);

  return NS_OK;
}

//------------------------------------------------------------------------------
//
// sbIJobProgressUI Implementation.
//
//------------------------------------------------------------------------------

/* attribute DOMString crop; */
NS_IMETHODIMP
sbAlbumViewScan::GetCrop(nsAString & aCrop)
{
  aCrop.AssignLiteral("end");
  return NS_OK;
}

//------------------------------------------------------------------------------
//
// sbIJobCancelable Implementation.
//
//------------------------------------------------------------------------------

/* boolean canCancel; */
NS_IMETHODIMP
sbAlbumViewScan::GetCanCancel(PRBool* _retval)
{
  TRACE(("sbAlbumViewScan[0x%8.x] - GetCanCancel", this));
  *_retval = PR_TRUE;
  return NS_OK;
}

/* void cancel(); */
NS_IMETHODIMP sbAlbumViewScan::Cancel()
{
  TRACE(("sbAlbumViewScan[0x%8.x] - Cancel", this));

  // Indicate that we have stopped
  mStatus = sbIJobProgress::STATUS_FAILED;
  UpdateProgress();
  return NS_OK;
}

//------------------------------------------------------------------------------
//
// nsITimerCallback Implementation.
//
//------------------------------------------------------------------------------

/* notify(in nsITimer aTimer); */
NS_IMETHODIMP
sbAlbumViewScan::Notify(nsITimer* aTimer)
{
  if (mNextAlbum) {
    ProcessNextAlbum();
  }
  return NS_OK;
}


//------------------------------------------------------------------------------
//
// sbIAlbumViewScan implementation.
//
//------------------------------------------------------------------------------
NS_IMETHODIMP
sbAlbumViewScan::ScanListForAlbums(sbIMediaList* aMediaList)
{
  TRACE(("sbAlbumViewScan[0x%8.x] - ScanListForAlbums", this));
  nsresult rv;
  
  nsCOMPtr<sbIMediaList> mediaList = aMediaList;

  // Create our view
  rv = CreateSortedView(mediaList);
  NS_ENSURE_SUCCESS(rv, rv);

  // Get the main library as a media list
  nsCOMPtr<sbILibraryManager> libManager =
      do_GetService("@songbirdnest.com/Songbird/library/Manager;1", &rv);
  NS_ENSURE_SUCCESS(rv, rv);

  nsCOMPtr<sbILibrary> library;
  rv = libManager->GetMainLibrary(getter_AddRefs(library));
  NS_ENSURE_SUCCESS(rv, rv);

  mLibraryList = do_QueryInterface(library, &rv);
  NS_ENSURE_SUCCESS(rv, rv);

  // Setup the initial scan details
  mStatus = sbIJobProgress::STATUS_RUNNING;
  mCurrentItemCount = 0;
  mNextAlbum = PR_TRUE;
  
  // Get how many items we have to process
  rv = mMediaListView->GetLength(&mTotalItemCount);
  NS_ENSURE_SUCCESS(rv, rv);
  UpdateProgress();

  // Start up the interval timer that will process the items
  // Create our timer
  mIntervalTimer = do_CreateInstance(NS_TIMER_CONTRACTID, &rv);
  NS_ENSURE_SUCCESS(rv, rv);
  rv = mIntervalTimer->InitWithCallback(this,
                                        10,
                                        nsITimer::TYPE_REPEATING_SLACK);
  NS_ENSURE_SUCCESS(rv, rv);

  return NS_OK;
}

//------------------------------------------------------------------------------
//
// sbAlbumViewScan implementation.
//
//------------------------------------------------------------------------------
sbAlbumViewScan::sbAlbumViewScan() :
  mStatus(sbIJobProgress::STATUS_SUCCEEDED),
  mTotalItemCount(0),
  mCurrentItemCount(0),
  mNextAlbum(PR_FALSE)
{
#ifdef PR_LOGGING
  if (!gAlbumViewsLog) {
    gAlbumViewsLog = PR_NewLogModule("sbAlbumViews");
  }
#endif
  TRACE(("sbAlbumViewScan::Constructed [0x%.8x]", this));
}

sbAlbumViewScan::~sbAlbumViewScan()
{
  TRACE(("sbAlbumViewScan::Destructed [0x%.8x]", this));
}

nsresult
sbAlbumViewScan::CreateSortedView(sbIMediaList* aMediaList)
{
  TRACE(("sbAlbumViewScan[0x%.8x] - CreateSortedView", this));
  nsresult rv;
  
  // Create the view
  rv = aMediaList->CreateView(nsnull, getter_AddRefs(mMediaListView));
  NS_ENSURE_SUCCESS(rv, rv);

  // Filter and sort the view
  nsCOMPtr<sbIFilterableMediaListView> filterView =
    do_QueryInterface(mMediaListView, &rv);
  NS_ENSURE_SUCCESS(rv, rv);

  // get the old constraints
  nsCOMPtr<sbILibraryConstraint> constraint;
  rv = filterView->GetFilterConstraint(getter_AddRefs(constraint));
  NS_ENSURE_SUCCESS(rv, rv);

  nsCOMPtr<sbILibraryConstraintBuilder> builder =
    do_CreateInstance("@songbirdnest.com/Songbird/Library/ConstraintBuilder;1",
                      &rv);
  NS_ENSURE_SUCCESS(rv, rv);

  // push the original constraints into it if there's an existing one
  if (constraint) {
    rv = builder->IncludeConstraint(constraint, nsnull);
    NS_ENSURE_SUCCESS(rv, rv);
    rv = builder->Intersect(nsnull);
    NS_ENSURE_SUCCESS(rv, rv);
  }

  // Add the isList and Hidden filters
  rv = builder->Include(NS_LITERAL_STRING(SB_PROPERTY_ISLIST),
                        NS_LITERAL_STRING("0"),
                        nsnull);
  NS_ENSURE_SUCCESS(rv, rv);
  rv = builder->Intersect(nsnull);
  NS_ENSURE_SUCCESS(rv, rv);
  rv = builder->Include(NS_LITERAL_STRING(SB_PROPERTY_HIDDEN),
                        NS_LITERAL_STRING("0"),
                        nsnull);
  NS_ENSURE_SUCCESS(rv, rv);
  //rv = builder->Intersect(nsnull);
  //NS_ENSURE_SUCCESS(rv, rv);
  
  // This stuffs things up because the view will be modified while it is still
  // being iterated!

  // Also filter out items that have already been scanned
  //rv = builder->Include(NS_LITERAL_STRING(SB_ALBUM_PROP),
  //                      EmptyString(),
  //                      nsnull);
  //NS_ENSURE_SUCCESS(rv, rv);

  // Now reset the constraint on the view
  rv = builder->Get(getter_AddRefs(constraint));
  NS_ENSURE_SUCCESS(rv, rv);
  rv = filterView->SetFilterConstraint(constraint);
  NS_ENSURE_SUCCESS(rv, rv);

  // Now sort the items
  // Sort by:
  //  AlbumName       - Asc
  //  AlbumArtistName - Asc
  //  ArtistName      - Asc
  //  Disc Number     - Asc
  //  Track Number    - Asc
  nsCOMPtr<sbIMutablePropertyArray> newSort =
    do_CreateInstance(SB_MUTABLEPROPERTYARRAY_CONTRACTID, &rv);
  NS_ENSURE_SUCCESS(rv, rv);
  
  rv = newSort->SetStrict(PR_FALSE);
  NS_ENSURE_SUCCESS(rv, rv);
  
  // AlbumName
  rv = newSort->AppendProperty(NS_LITERAL_STRING(SB_PROPERTY_ALBUMNAME),
                               NS_LITERAL_STRING("a"));
  NS_ENSURE_SUCCESS(rv, rv);

  // AlbumArtistName
  rv = newSort->AppendProperty(NS_LITERAL_STRING(SB_PROPERTY_ALBUMARTISTNAME),
                               NS_LITERAL_STRING("a"));
  NS_ENSURE_SUCCESS(rv, rv);

  // ArtistName
  rv = newSort->AppendProperty(NS_LITERAL_STRING(SB_PROPERTY_ARTISTNAME),
                               NS_LITERAL_STRING("a"));
  NS_ENSURE_SUCCESS(rv, rv);

  // Disc Number
  rv = newSort->AppendProperty(NS_LITERAL_STRING(SB_PROPERTY_DISCNUMBER),
                               NS_LITERAL_STRING("a"));
  NS_ENSURE_SUCCESS(rv, rv);

  // Track Number
  rv = newSort->AppendProperty(NS_LITERAL_STRING(SB_PROPERTY_TRACKNUMBER),
                               NS_LITERAL_STRING("a"));
  NS_ENSURE_SUCCESS(rv, rv);

  // Now set the sort on this list
  nsCOMPtr<sbISortableMediaListView> sortable =
    do_QueryInterface(mMediaListView, &rv);
  NS_ENSURE_SUCCESS(rv, rv);

  rv = sortable->SetSort(newSort);
  NS_ENSURE_SUCCESS(rv, rv);

  return NS_OK;
}

nsresult
sbAlbumViewScan::ProcessNextAlbum()
{
  TRACE(("sbAlbumViewScan[0x%.8x] - ProcessNextAlbum", this));
  nsresult rv = NS_OK;
  
  mNextAlbum = PR_FALSE;
  
  if (mCurrentItemCount >= mTotalItemCount) {
    mStatus = sbIJobProgress::STATUS_SUCCEEDED;
    UpdateProgress();
    return NS_OK;
  }

  // Variables for keeping track of our last information
  nsString lastAlbumName;
  nsString lastArtistName;
  nsString lastDiscNumber;
  nsString firstFoundImage;

  // Grab the properties we need all at once
  nsCOMPtr<sbIMutablePropertyArray> propList =
    do_CreateInstance(SB_MUTABLEPROPERTYARRAY_CONTRACTID, &rv);
  NS_ENSURE_SUCCESS(rv, rv);

  // Don't validate since we are not putting values
  rv = propList->SetStrict(PR_FALSE);
  NS_ENSURE_SUCCESS(rv, rv);

  // AlbumName, ArtistName, AlbumArtistName, DiscNumber, Album GUID
  rv = propList->AppendProperty(NS_LITERAL_STRING(SB_PROPERTY_ALBUMNAME),
                                EmptyString());
  NS_ENSURE_SUCCESS(rv, rv);
  rv = propList->AppendProperty(NS_LITERAL_STRING(SB_PROPERTY_ARTISTNAME),
                                EmptyString());
  NS_ENSURE_SUCCESS(rv, rv);
  rv = propList->AppendProperty(NS_LITERAL_STRING(SB_PROPERTY_ALBUMARTISTNAME),
                                EmptyString());
  NS_ENSURE_SUCCESS(rv, rv);
  rv = propList->AppendProperty(NS_LITERAL_STRING(SB_PROPERTY_DISCNUMBER),
                                EmptyString());
  NS_ENSURE_SUCCESS(rv, rv);
  rv = propList->AppendProperty(NS_LITERAL_STRING(SB_ALBUM_PROP),
                                EmptyString());
  NS_ENSURE_SUCCESS(rv, rv);

  // TODO: Make this as fast as possible!!!!

  // Loop while we still have items and we haven't gotten to the next album
  // We need to check the albumName first with the previous one, then if that
  // matches we can check the albumArtist/artist with previous artist.
  // We also use the disc number to separate multiple disc albums.
  // If all that matches then we are still on the same album.
  while (mCurrentItemCount < mTotalItemCount) {
    TRACE(("sbAlbumViewScan::ProcessNextAlbum - Processing item %d of %d",
           mCurrentItemCount,
           mTotalItemCount));

    nsCOMPtr<sbIMediaItem> item;
    rv = mMediaListView->GetItemByIndex(mCurrentItemCount,
                                        getter_AddRefs(item));
    if (NS_FAILED(rv)) {
      // Move on to the next item
      TRACE(("sbAlbumViewScan::ProcessNextAlbum - Item failed with %08X", rv));
      mCurrentItemCount++;
      continue;
    }

    nsCOMPtr<sbIPropertyArray> propListValues;
    rv = item->GetProperties(propList, getter_AddRefs(propListValues));
    NS_ENSURE_SUCCESS(rv, rv);

    // See if this item already has a guid, if so skip it
    nsString guid;
    rv = propListValues->GetPropertyValue(NS_LITERAL_STRING(SB_ALBUM_PROP),
                                          guid);
    if (NS_SUCCEEDED(rv) && !guid.IsEmpty()) {
      // Move on to the next item
      mCurrentItemCount++;
      continue;
    }

    // We need an album name or this is completely pointless.
    nsString albumName;
    rv = propListValues->GetPropertyValue(NS_LITERAL_STRING(SB_PROPERTY_ALBUMNAME),
                                          albumName);
    if (NS_FAILED(rv) || albumName.IsEmpty()) {
      // Move on to the next item
      mCurrentItemCount++;
      continue;
    }

    // Next we use either the albumArtistName (preferred) or artistName
    nsString artistName;
    rv = propListValues->GetPropertyValue(NS_LITERAL_STRING(SB_PROPERTY_ARTISTNAME),
                                          artistName);
    if (NS_FAILED(rv)) {
      // Move on to the next item
      mCurrentItemCount++;
      continue;
    }
    nsString albumArtistName;
    propListValues->GetPropertyValue(NS_LITERAL_STRING(SB_PROPERTY_ALBUMARTISTNAME),
                                     albumArtistName);
    // If the albumArtistName fails we will fall back to the artist name
    
    if (!albumArtistName.IsEmpty()) {
      // Try to use the album artist name if possible
      artistName = albumArtistName;
    }
    
    if (artistName.IsEmpty()) {
      // No artist then how are we going to know what album this belongs to?
      mCurrentItemCount++;
      continue;
    }
    
    // Get the disc number.
    nsString discNumber;
    rv = propListValues->GetPropertyValue(NS_LITERAL_STRING(SB_PROPERTY_DISCNUMBER),
                                          discNumber);
    if (NS_FAILED(rv) || discNumber.IsEmpty()) {
      discNumber.AssignLiteral("0");
    }

    TRACE(("sbAlbumViewScan::ProcessNextAlbum - Processing Album [%s] by [%s] Disc [%s]",
           NS_ConvertUTF16toUTF8(albumName).get(),
           NS_ConvertUTF16toUTF8(artistName).get(),
           NS_ConvertUTF16toUTF8(discNumber).get()
         ));

    // Construct GUID
    nsString newGUID = artistName;
    newGUID.AppendLiteral("#");
    newGUID.Append(albumName);
    newGUID.AppendLiteral("#");
    newGUID.Append(discNumber);

    // Set the albumGUID on this item
    TRACE(("sbAlbumViewScan::ProcessNextAlbum - Adding album GUID [%s]",
           NS_ConvertUTF16toUTF8(newGUID).get()));
    rv = item->SetProperty(NS_LITERAL_STRING(SB_ALBUM_PROP),
                           newGUID);

    mCurrentItemCount++;
    UpdateProgress();

    if (mCurrentItemCount % SCAN_BATCH_SIZE == 0) {
      break;
    }
  }
  
  // Found the album so update and give feed back to the user
  UpdateProgress();
  mNextAlbum = PR_TRUE;

  return NS_OK;
}

nsresult
sbAlbumViewScan::UpdateProgress()
{
  TRACE(("sbAlbumViewScan[0x%.8x] - UpdateProgress", this));
  
  if (mStatus != sbIJobProgress::STATUS_RUNNING) {
    mIntervalTimer->Cancel();
    mIntervalTimer = nsnull;
  }

  for (PRInt32 i = mListeners.Count() - 1; i >= 0; --i) {
    mListeners[i]->OnJobProgress(this);
  }

  return NS_OK;
}
